mirror of
https://github.com/esphome/esphome.git
synced 2026-01-17 23:44:52 -07:00
Compare commits
39 Commits
anova_safe
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db0b32bfc9 | ||
|
|
21794e28e5 | ||
|
|
728236270c | ||
|
|
01cdc4ed58 | ||
|
|
d6a0c8ffbb | ||
|
|
4cc0f874f7 | ||
|
|
ed58b9372f | ||
|
|
ee2a81923b | ||
|
|
0a1e7ee50b | ||
|
|
4d4283bcfa | ||
|
|
e4fb6988ff | ||
|
|
d31b733dce | ||
|
|
b25a2f8d8e | ||
|
|
3f892711c7 | ||
|
|
798d3bd956 | ||
|
|
77df3933db | ||
|
|
19514ccdf4 | ||
|
|
2947642ca5 | ||
|
|
60e333db08 | ||
|
|
d8463f4813 | ||
|
|
e1800d2fe2 | ||
|
|
50aa4b1992 | ||
|
|
edb303e495 | ||
|
|
973fc4c5dc | ||
|
|
f88e8fc43b | ||
|
|
d830787c71 | ||
|
|
c4c31a2e8e | ||
|
|
e6790f0042 | ||
|
|
ec7f72e280 | ||
|
|
6f29dbd6f1 | ||
|
|
9caf78aa7e | ||
|
|
1f4221abfa | ||
|
|
92808a09c7 | ||
|
|
e54d5ee898 | ||
|
|
bbe1155518 | ||
|
|
69d7b6e921 | ||
|
|
510c874061 | ||
|
|
f7ad324d81 | ||
|
|
58a9e30017 |
@@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
|
||||
}
|
||||
uint32_t field_length = res->as_uint32();
|
||||
ptr += consumed;
|
||||
if (ptr + field_length > end) {
|
||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||
return count; // Out of bounds
|
||||
}
|
||||
ptr += field_length;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
|
||||
if (ptr + 4 > end) {
|
||||
if (end - ptr < 4) {
|
||||
return count;
|
||||
}
|
||||
ptr += 4;
|
||||
@@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
}
|
||||
uint32_t field_length = res->as_uint32();
|
||||
ptr += consumed;
|
||||
if (ptr + field_length > end) {
|
||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_FIXED32: { // 32-bit
|
||||
if (ptr + 4 > end) {
|
||||
if (end - ptr < 4) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,12 +158,14 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_offset_calibration_) {
|
||||
// Initialize flash storage for offset calibrations
|
||||
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_);
|
||||
uint32_t o_hash = fnv1_hash("_offset_calibration_");
|
||||
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
|
||||
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
||||
this->restore_offset_calibrations_();
|
||||
|
||||
// Initialize flash storage for power offset calibrations
|
||||
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_);
|
||||
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
|
||||
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
|
||||
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
||||
this->restore_power_offset_calibrations_();
|
||||
} else {
|
||||
@@ -183,7 +185,8 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_gain_calibration_) {
|
||||
// Initialize flash storage for gain calibration
|
||||
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_);
|
||||
uint32_t g_hash = fnv1_hash("_gain_calibration_");
|
||||
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
|
||||
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
||||
this->restore_gain_calibrations_();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include "hmac_sha256.h"
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST)
|
||||
@@ -26,9 +25,7 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_
|
||||
void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); }
|
||||
|
||||
void HmacSHA256::get_hex(char *output) {
|
||||
for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) {
|
||||
sprintf(output + (i * 2), "%02x", this->digest_[i]);
|
||||
}
|
||||
format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE);
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_bytes(const uint8_t *expected) {
|
||||
|
||||
@@ -242,9 +242,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t content_length = container->content_length;
|
||||
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
|
||||
|
||||
size_t max_length = this->max_response_buffer_size_;
|
||||
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||
if (this->capture_response_.value(x...)) {
|
||||
std::string response_body;
|
||||
|
||||
@@ -213,18 +213,12 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
return 0;
|
||||
this->feed_wdt();
|
||||
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
|
||||
this->feed_wdt();
|
||||
if (read_len > 0) {
|
||||
this->bytes_read_ += read_len;
|
||||
}
|
||||
|
||||
this->feed_wdt();
|
||||
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
|
||||
this->feed_wdt();
|
||||
this->bytes_read_ += read_len;
|
||||
|
||||
this->duration_ms += (millis() - start);
|
||||
|
||||
return read_len;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation, pins
|
||||
@@ -18,13 +19,16 @@ from esphome.const import (
|
||||
CONF_ROTATION,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.core import ID, EnumValue
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import add_class_to_obj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import boards, hub75_ns
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@stuartparmenter"]
|
||||
|
||||
@@ -120,13 +124,51 @@ PANEL_LAYOUTS = {
|
||||
}
|
||||
|
||||
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
|
||||
SCAN_PATTERNS = {
|
||||
SCAN_WIRINGS = {
|
||||
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
|
||||
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
|
||||
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
|
||||
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
|
||||
"SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH,
|
||||
"SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH,
|
||||
"SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH,
|
||||
"SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH,
|
||||
}
|
||||
|
||||
# Deprecated scan wiring names - mapped to new names
|
||||
DEPRECATED_SCAN_WIRINGS = {
|
||||
"FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH",
|
||||
"FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH",
|
||||
"FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH",
|
||||
}
|
||||
|
||||
|
||||
def _validate_scan_wiring(value):
|
||||
"""Validate scan_wiring with deprecation warnings for old names."""
|
||||
value = cv.string(value).upper().replace(" ", "_")
|
||||
|
||||
# Check if using deprecated name
|
||||
# Remove deprecated names in 2026.7.0
|
||||
if value in DEPRECATED_SCAN_WIRINGS:
|
||||
new_name = DEPRECATED_SCAN_WIRINGS[value]
|
||||
_LOGGER.warning(
|
||||
"Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. "
|
||||
"Please use '%s' instead.",
|
||||
value,
|
||||
new_name,
|
||||
)
|
||||
value = new_name
|
||||
|
||||
# Validate against allowed values
|
||||
if value not in SCAN_WIRINGS:
|
||||
raise cv.Invalid(
|
||||
f"Unknown scan wiring '{value}'. "
|
||||
f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}"
|
||||
)
|
||||
|
||||
# Return as EnumValue like cv.enum does
|
||||
result = add_class_to_obj(value, EnumValue)
|
||||
result.enum_value = SCAN_WIRINGS[value]
|
||||
return result
|
||||
|
||||
|
||||
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
|
||||
CLOCK_SPEEDS = {
|
||||
"8MHZ": Hub75ClockSpeed.HZ_8M,
|
||||
@@ -382,9 +424,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
|
||||
cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
|
||||
# Panel hardware configuration
|
||||
cv.Optional(CONF_SCAN_WIRING): cv.enum(
|
||||
SCAN_PATTERNS, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring,
|
||||
cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
|
||||
# Display configuration
|
||||
cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
|
||||
@@ -547,7 +587,7 @@ def _build_config_struct(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
add_idf_component(
|
||||
name="esphome/esp-hub75",
|
||||
ref="0.2.2",
|
||||
ref="0.3.0",
|
||||
)
|
||||
|
||||
# Set compile-time configuration via build flags (so external library sees them)
|
||||
|
||||
@@ -19,12 +19,12 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
|
||||
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
|
||||
this->raw_timings_ = &timings;
|
||||
this->packed_data_ = nullptr;
|
||||
this->base85_ptr_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_raw_timings_base85(const std::string &base85) {
|
||||
this->base85_ptr_ = &base85;
|
||||
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
|
||||
this->base64url_ptr_ = &base64url;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->packed_data_ = nullptr;
|
||||
return *this;
|
||||
@@ -35,7 +35,7 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t
|
||||
this->packed_length_ = length;
|
||||
this->packed_count_ = count;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->base85_ptr_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -101,13 +101,22 @@ void Infrared::control(const InfraredCall &call) {
|
||||
call.get_packed_count());
|
||||
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(),
|
||||
call.get_repeat_count());
|
||||
} else if (call.is_base85()) {
|
||||
// Decode base85 directly into transmit buffer (zero heap allocations)
|
||||
if (!transmit_data->set_data_from_base85(call.get_base85_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base85 data");
|
||||
} else if (call.is_base64url()) {
|
||||
// Decode base64url (URL-safe) into transmit buffer
|
||||
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base64url data");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base85 raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
|
||||
// Sanity check: validate timing values are within reasonable bounds
|
||||
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
|
||||
for (int32_t timing : transmit_data->get_data()) {
|
||||
int32_t abs_timing = timing < 0 ? -timing : timing;
|
||||
if (abs_timing > max_timing_us) {
|
||||
ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
|
||||
call.get_repeat_count());
|
||||
} else {
|
||||
// From vector (lambdas/automations)
|
||||
|
||||
@@ -40,11 +40,11 @@ class InfraredCall {
|
||||
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
|
||||
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
|
||||
|
||||
/// Set the raw timings from base85-encoded int32 data
|
||||
/// Set the raw timings from base64url-encoded little-endian int32 data
|
||||
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
|
||||
/// @note Usage: For web_server where the encoded string is on the stack.
|
||||
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
|
||||
/// @note Decoding happens at perform() time, directly into the transmit buffer.
|
||||
InfraredCall &set_raw_timings_base85(const std::string &base85);
|
||||
InfraredCall &set_raw_timings_base64url(const std::string &base64url);
|
||||
|
||||
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
|
||||
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
|
||||
@@ -59,18 +59,18 @@ class InfraredCall {
|
||||
|
||||
/// Get the carrier frequency
|
||||
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
|
||||
/// Get the raw timings (only valid if set via set_raw_timings, not packed or base85)
|
||||
/// Get the raw timings (only valid if set via set_raw_timings)
|
||||
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
|
||||
/// Check if raw timings have been set (vector, packed, or base85)
|
||||
/// Check if raw timings have been set (any format)
|
||||
bool has_raw_timings() const {
|
||||
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base85_ptr_ != nullptr;
|
||||
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
|
||||
}
|
||||
/// Check if using packed data format
|
||||
bool is_packed() const { return this->packed_data_ != nullptr; }
|
||||
/// Check if using base85 data format
|
||||
bool is_base85() const { return this->base85_ptr_ != nullptr; }
|
||||
/// Get the base85 data string
|
||||
const std::string &get_base85_data() const { return *this->base85_ptr_; }
|
||||
/// Check if using base64url data format
|
||||
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
|
||||
/// Get the base64url data string
|
||||
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
|
||||
/// Get packed data (only valid if set via set_raw_timings_packed)
|
||||
const uint8_t *get_packed_data() const { return this->packed_data_; }
|
||||
uint16_t get_packed_length() const { return this->packed_length_; }
|
||||
@@ -84,8 +84,8 @@ class InfraredCall {
|
||||
optional<uint32_t> carrier_frequency_;
|
||||
// Pointer to vector-based timings (caller-owned, must outlive perform())
|
||||
const std::vector<int32_t> *raw_timings_{nullptr};
|
||||
// Pointer to base85-encoded string (caller-owned, must outlive perform())
|
||||
const std::string *base85_ptr_{nullptr};
|
||||
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
|
||||
const std::string *base64url_ptr_{nullptr};
|
||||
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
|
||||
const uint8_t *packed_data_{nullptr};
|
||||
uint16_t packed_length_{0};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "light_json_schema.h"
|
||||
#include "color_mode.h"
|
||||
#include "light_output.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
@@ -8,29 +9,32 @@ namespace esphome::light {
|
||||
|
||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||
|
||||
// Get JSON string for color mode using linear search (avoids large switch jump table)
|
||||
static const char *get_color_mode_json_str(ColorMode mode) {
|
||||
// Parallel arrays: mode values and their corresponding strings
|
||||
// Uses less RAM than a switch jump table on sparse enum values
|
||||
static constexpr ColorMode MODES[] = {
|
||||
ColorMode::ON_OFF,
|
||||
ColorMode::BRIGHTNESS,
|
||||
ColorMode::WHITE,
|
||||
ColorMode::COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB,
|
||||
ColorMode::RGB_WHITE,
|
||||
ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE,
|
||||
};
|
||||
static constexpr const char *STRINGS[] = {
|
||||
"onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) {
|
||||
if (MODES[i] == mode)
|
||||
return STRINGS[i];
|
||||
// Get JSON string for color mode.
|
||||
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would
|
||||
// generate a large jump table. Converting to bit index (0-9) allows a compact switch.
|
||||
static ProgmemStr get_color_mode_json_str(ColorMode mode) {
|
||||
switch (ColorModeBitPolicy::to_bit(mode)) {
|
||||
case 1:
|
||||
return ESPHOME_F("onoff");
|
||||
case 2:
|
||||
return ESPHOME_F("brightness");
|
||||
case 3:
|
||||
return ESPHOME_F("white");
|
||||
case 4:
|
||||
return ESPHOME_F("color_temp");
|
||||
case 5:
|
||||
return ESPHOME_F("cwww");
|
||||
case 6:
|
||||
return ESPHOME_F("rgb");
|
||||
case 7:
|
||||
return ESPHOME_F("rgbw");
|
||||
case 8:
|
||||
return ESPHOME_F("rgbct");
|
||||
case 9:
|
||||
return ESPHOME_F("rgbww");
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
@@ -44,7 +48,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
auto values = state.remote_values;
|
||||
|
||||
const auto color_mode = values.get_color_mode();
|
||||
const char *mode_str = get_color_mode_json_str(color_mode);
|
||||
const auto *mode_str = get_color_mode_json_str(color_mode);
|
||||
if (mode_str != nullptr) {
|
||||
root[ESPHOME_F("color_mode")] = mode_str;
|
||||
}
|
||||
|
||||
@@ -271,24 +271,31 @@ class ServerRegister {
|
||||
|
||||
// Formats a raw value into a string representation based on the value type for debugging
|
||||
std::string format_value(int64_t value) const {
|
||||
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
|
||||
// plus null terminator = 43, rounded to 44 for 4-byte alignment
|
||||
char buf[44];
|
||||
switch (this->value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::U_QWORD_R:
|
||||
return std::to_string(static_cast<uint64_t>(value));
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
|
||||
return buf;
|
||||
case SensorValueType::S_WORD:
|
||||
case SensorValueType::S_DWORD:
|
||||
case SensorValueType::S_DWORD_R:
|
||||
case SensorValueType::S_QWORD:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
return std::to_string(value);
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
case SensorValueType::FP32_R:
|
||||
case SensorValueType::FP32:
|
||||
return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||
return buf;
|
||||
default:
|
||||
return std::to_string(value);
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,20 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
|
||||
while ((items_left > 0) && index < data.size()) {
|
||||
uint8_t b = data[index];
|
||||
switch (this->encode_) {
|
||||
case RawEncoding::HEXBYTES:
|
||||
output_str += str_snprintf("%02x", 2, b);
|
||||
case RawEncoding::HEXBYTES: {
|
||||
// max 3: 2 hex digits + null
|
||||
char hex_buf[3];
|
||||
snprintf(hex_buf, sizeof(hex_buf), "%02x", b);
|
||||
output_str += hex_buf;
|
||||
break;
|
||||
case RawEncoding::COMMA:
|
||||
output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b);
|
||||
}
|
||||
case RawEncoding::COMMA: {
|
||||
// max 5: optional ','(1) + uint8(3) + null, for both ",%d" and "%d"
|
||||
char dec_buf[5];
|
||||
snprintf(dec_buf, sizeof(dec_buf), index != this->offset ? ",%d" : "%d", b);
|
||||
output_str += dec_buf;
|
||||
break;
|
||||
}
|
||||
case RawEncoding::ANSI:
|
||||
if (b < 0x20)
|
||||
break;
|
||||
|
||||
@@ -43,6 +43,14 @@ namespace network {
|
||||
/// Buffer size for IP address string (IPv6 max: 39 chars + null)
|
||||
static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40;
|
||||
|
||||
/// Lowercase hex digits in IP address string (A-F -> a-f for IPv6 per RFC 5952)
|
||||
inline void lowercase_ip_str(char *buf) {
|
||||
for (char *p = buf; *p; ++p) {
|
||||
if (*p >= 'A' && *p <= 'F')
|
||||
*p += 32;
|
||||
}
|
||||
}
|
||||
|
||||
struct IPAddress {
|
||||
public:
|
||||
#ifdef USE_HOST
|
||||
@@ -52,10 +60,15 @@ struct IPAddress {
|
||||
}
|
||||
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
|
||||
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
|
||||
std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); }
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
return buf;
|
||||
}
|
||||
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
|
||||
char *str_to(char *buf) const {
|
||||
return const_cast<char *>(inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE));
|
||||
inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
return buf; // IPv4 only, no hex letters to lowercase
|
||||
}
|
||||
#else
|
||||
IPAddress() { ip_addr_set_zero(&ip_addr_); }
|
||||
@@ -134,9 +147,18 @@ struct IPAddress {
|
||||
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
|
||||
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
|
||||
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
|
||||
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
return buf;
|
||||
}
|
||||
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
|
||||
char *str_to(char *buf) const { return ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); }
|
||||
/// Output is lowercased per RFC 5952 (IPv6 hex digits a-f).
|
||||
char *str_to(char *buf) const {
|
||||
ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
lowercase_ip_str(buf);
|
||||
return buf;
|
||||
}
|
||||
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
IPAddress &operator+=(uint8_t increase) {
|
||||
|
||||
@@ -561,8 +561,9 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
|
||||
}
|
||||
|
||||
void OpenTherm::debug_data(OpenthermData &data) {
|
||||
ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(),
|
||||
format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str());
|
||||
char type_buf[9], id_buf[9], hb_buf[9], lb_buf[9];
|
||||
ESP_LOGD(TAG, "%s %s %s %s", format_bin_to(type_buf, data.type), format_bin_to(id_buf, data.id),
|
||||
format_bin_to(hb_buf, data.valueHB), format_bin_to(lb_buf, data.valueLB));
|
||||
ESP_LOGD(TAG, "type: %s; id: %u; HB: %u; LB: %u; uint_16: %u; float: %f",
|
||||
this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.u16(),
|
||||
data.f88());
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "pipsolar.output";
|
||||
|
||||
void PipsolarOutput::write_state(float state) {
|
||||
char tmp[10];
|
||||
sprintf(tmp, this->set_command_.c_str(), state);
|
||||
snprintf(tmp, sizeof(tmp), this->set_command_, state);
|
||||
|
||||
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
|
||||
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
|
||||
|
||||
@@ -15,13 +15,15 @@ class PipsolarOutput : public output::FloatOutput {
|
||||
public:
|
||||
PipsolarOutput() {}
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; }
|
||||
void set_set_command(const std::string &command) { this->set_command_ = command; };
|
||||
void set_set_command(const char *command) { this->set_command_ = command; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_set_command(const std::string &command) = delete;
|
||||
void set_possible_values(std::vector<float> possible_values) { this->possible_values_ = std::move(possible_values); }
|
||||
void set_value(float value) { this->write_state(value); };
|
||||
void set_value(float value) { this->write_state(value); }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
std::string set_command_;
|
||||
const char *set_command_{nullptr};
|
||||
Pipsolar *parent_;
|
||||
std::vector<float> possible_values_;
|
||||
};
|
||||
|
||||
@@ -9,14 +9,9 @@ static const char *const TAG = "pipsolar.switch";
|
||||
|
||||
void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
|
||||
void PipsolarSwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
if (!this->on_command_.empty()) {
|
||||
this->parent_->queue_command(this->on_command_);
|
||||
}
|
||||
} else {
|
||||
if (!this->off_command_.empty()) {
|
||||
this->parent_->queue_command(this->off_command_);
|
||||
}
|
||||
const char *command = state ? this->on_command_ : this->off_command_;
|
||||
if (command != nullptr) {
|
||||
this->parent_->queue_command(command);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,18 @@ namespace pipsolar {
|
||||
class Pipsolar;
|
||||
class PipsolarSwitch : public switch_::Switch, public Component {
|
||||
public:
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; };
|
||||
void set_on_command(const std::string &command) { this->on_command_ = command; };
|
||||
void set_off_command(const std::string &command) { this->off_command_ = command; };
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; }
|
||||
void set_on_command(const char *command) { this->on_command_ = command; }
|
||||
void set_off_command(const char *command) { this->off_command_ = command; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_on_command(const std::string &command) = delete;
|
||||
void set_off_command(const std::string &command) = delete;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
std::string on_command_;
|
||||
std::string off_command_;
|
||||
const char *on_command_{nullptr};
|
||||
const char *off_command_{nullptr};
|
||||
Pipsolar *parent_;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace remote_base {
|
||||
|
||||
@@ -160,8 +158,8 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoteTransmitData::set_data_from_base85(const std::string &base85) {
|
||||
return base85_decode_int32_vector(base85, this->data_);
|
||||
bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
|
||||
return base64_decode_int32_vector(base64url, this->data_);
|
||||
}
|
||||
|
||||
/* RemoteTransmitterBase */
|
||||
|
||||
@@ -36,11 +36,11 @@ class RemoteTransmitData {
|
||||
/// @param len Length of the buffer in bytes
|
||||
/// @param count Number of values (for reserve optimization)
|
||||
void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count);
|
||||
/// Set data from base85-encoded int32 values
|
||||
/// Decodes directly into internal buffer (zero heap allocations)
|
||||
/// @param base85 Base85-encoded string (5 chars per int32 value)
|
||||
/// Set data from base64url-encoded little-endian int32 values
|
||||
/// Base64url is URL-safe: uses '-' instead of '+', '_' instead of '/'
|
||||
/// @param base64url Base64url-encoded string of little-endian int32 values
|
||||
/// @return true if successful, false if decode failed or invalid size
|
||||
bool set_data_from_base85(const std::string &base85);
|
||||
bool set_data_from_base64url(const std::string &base64url);
|
||||
void reset() {
|
||||
this->data_.clear();
|
||||
this->carrier_frequency_ = 0;
|
||||
|
||||
@@ -14,7 +14,9 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
void set_parent(Sun *parent) { parent_ = parent; }
|
||||
void set_elevation(double elevation) { elevation_ = elevation; }
|
||||
void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
|
||||
void set_format(const std::string &format) { format_ = format; }
|
||||
void set_format(const char *format) { this->format_ = format; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_format(const std::string &format) = delete;
|
||||
|
||||
void update() override {
|
||||
optional<ESPTime> res;
|
||||
@@ -29,14 +31,14 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
}
|
||||
|
||||
char buf[ESPTime::STRFTIME_BUFFER_SIZE];
|
||||
size_t len = res->strftime_to(buf, this->format_.c_str());
|
||||
size_t len = res->strftime_to(buf, this->format_);
|
||||
this->publish_state(buf, len);
|
||||
}
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
std::string format_{};
|
||||
const char *format_{nullptr};
|
||||
Sun *parent_;
|
||||
double elevation_;
|
||||
bool sunrise_;
|
||||
|
||||
@@ -118,8 +118,7 @@ async def to_code(config):
|
||||
var = await alarm_control_panel.new_alarm_control_panel(config)
|
||||
await cg.register_component(var, config)
|
||||
if CONF_CODES in config:
|
||||
for acode in config[CONF_CODES]:
|
||||
cg.add(var.add_code(acode))
|
||||
cg.add(var.set_codes(config[CONF_CODES]))
|
||||
if CONF_REQUIRES_CODE_TO_ARM in config:
|
||||
cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM]))
|
||||
|
||||
|
||||
@@ -206,7 +206,13 @@ bool TemplateAlarmControlPanel::is_code_valid_(optional<std::string> code) {
|
||||
if (!this->codes_.empty()) {
|
||||
if (code.has_value()) {
|
||||
ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str());
|
||||
return (std::count(this->codes_.begin(), this->codes_.end(), code.value()) == 1);
|
||||
// Use strcmp for const char* comparison
|
||||
const char *code_cstr = code.value().c_str();
|
||||
for (const char *stored_code : this->codes_) {
|
||||
if (strcmp(stored_code, code_cstr) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "No code provided");
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
@@ -86,11 +87,14 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED);
|
||||
#endif
|
||||
|
||||
/** add a code
|
||||
/** Set the codes (from initializer list).
|
||||
*
|
||||
* @param code The code
|
||||
* @param codes The list of valid codes
|
||||
*/
|
||||
void add_code(const std::string &code) { this->codes_.push_back(code); }
|
||||
void set_codes(std::initializer_list<const char *> codes) { this->codes_ = codes; }
|
||||
|
||||
// Deleted overload to catch incorrect std::string usage at compile time
|
||||
void set_codes(std::initializer_list<std::string> codes) = delete;
|
||||
|
||||
/** set requires a code to arm
|
||||
*
|
||||
@@ -155,8 +159,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
uint32_t pending_time_;
|
||||
// the time in trigger
|
||||
uint32_t trigger_time_;
|
||||
// a list of codes
|
||||
std::vector<std::string> codes_;
|
||||
// a list of codes (const char* pointers to string literals in flash)
|
||||
FixedVector<const char *> codes_;
|
||||
// requires a code to arm
|
||||
bool requires_code_to_arm_ = false;
|
||||
bool supports_arm_home_ = false;
|
||||
|
||||
@@ -8,16 +8,23 @@ static const char *const TAG = "template.text";
|
||||
void TemplateText::setup() {
|
||||
if (this->f_.has_value())
|
||||
return;
|
||||
std::string value = this->initial_value_;
|
||||
if (!this->pref_) {
|
||||
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
|
||||
} else {
|
||||
uint32_t key = this->get_preference_hash();
|
||||
key += this->traits.get_min_length() << 2;
|
||||
key += this->traits.get_max_length() << 4;
|
||||
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
|
||||
this->pref_->setup(key, value);
|
||||
|
||||
if (this->pref_ == nullptr) {
|
||||
// No restore - use const char* directly, no heap allocation needed
|
||||
if (this->initial_value_ != nullptr && this->initial_value_[0] != '\0') {
|
||||
ESP_LOGD(TAG, "State from initial: %s", this->initial_value_);
|
||||
this->publish_state(this->initial_value_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Need std::string for pref_->setup() to fill from flash
|
||||
std::string value{this->initial_value_ != nullptr ? this->initial_value_ : ""};
|
||||
uint32_t key = this->get_preference_hash();
|
||||
key += this->traits.get_min_length() << 2;
|
||||
key += this->traits.get_max_length() << 4;
|
||||
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
|
||||
this->pref_->setup(key, value);
|
||||
if (!value.empty())
|
||||
this->publish_state(value);
|
||||
}
|
||||
|
||||
@@ -70,13 +70,15 @@ class TemplateText final : public text::Text, public PollingComponent {
|
||||
|
||||
Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; }
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void set_initial_value(const std::string &initial_value) { this->initial_value_ = initial_value; }
|
||||
void set_initial_value(const char *initial_value) { this->initial_value_ = initial_value; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_initial_value(const std::string &initial_value) = delete;
|
||||
void set_value_saver(TemplateTextSaverBase *restore_value_saver) { this->pref_ = restore_value_saver; }
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
bool optimistic_ = false;
|
||||
std::string initial_value_;
|
||||
const char *initial_value_{nullptr};
|
||||
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
||||
TemplateLambda<std::string> f_{};
|
||||
|
||||
|
||||
@@ -108,8 +108,7 @@ async def to_code(config):
|
||||
cg.add(var.set_broadcast_port(conf_port[CONF_BROADCAST_PORT]))
|
||||
if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255":
|
||||
cg.add(var.set_listen_address(listen_address))
|
||||
for address in config[CONF_ADDRESSES]:
|
||||
cg.add(var.add_address(str(address)))
|
||||
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
|
||||
if on_receive := config.get(CONF_ON_RECEIVE):
|
||||
on_receive = on_receive[0]
|
||||
trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "udp_component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace udp {
|
||||
namespace esphome::udp {
|
||||
|
||||
static const char *const TAG = "udp";
|
||||
|
||||
@@ -95,7 +94,7 @@ void UDPComponent::setup() {
|
||||
// 8266 and RP2040 `Duino
|
||||
for (const auto &address : this->addresses_) {
|
||||
auto ipaddr = IPAddress();
|
||||
ipaddr.fromString(address.c_str());
|
||||
ipaddr.fromString(address);
|
||||
this->ipaddrs_.push_back(ipaddr);
|
||||
}
|
||||
if (this->should_listen_)
|
||||
@@ -130,8 +129,8 @@ void UDPComponent::dump_config() {
|
||||
" Listen Port: %u\n"
|
||||
" Broadcast Port: %u",
|
||||
this->listen_port_, this->broadcast_port_);
|
||||
for (const auto &address : this->addresses_)
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address.c_str());
|
||||
for (const char *address : this->addresses_)
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address);
|
||||
if (this->listen_address_.has_value()) {
|
||||
char addr_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf));
|
||||
@@ -162,7 +161,6 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
} // namespace esphome::udp
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
#include "esphome/components/socket/socket.h"
|
||||
@@ -9,15 +10,17 @@
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
#include <WiFiUdp.h>
|
||||
#endif
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace udp {
|
||||
namespace esphome::udp {
|
||||
|
||||
static const size_t MAX_PACKET_SIZE = 508;
|
||||
class UDPComponent : public Component {
|
||||
public:
|
||||
void add_address(const char *addr) { this->addresses_.emplace_back(addr); }
|
||||
void set_addresses(std::initializer_list<const char *> addresses) { this->addresses_ = addresses; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_addresses(std::initializer_list<std::string> addresses) = delete;
|
||||
void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); }
|
||||
void set_listen_port(uint16_t port) { this->listen_port_ = port; }
|
||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||
@@ -49,11 +52,10 @@ class UDPComponent : public Component {
|
||||
std::vector<IPAddress> ipaddrs_{};
|
||||
WiFiUDP udp_client_{};
|
||||
#endif
|
||||
std::vector<std::string> addresses_{};
|
||||
FixedVector<const char *> addresses_{};
|
||||
|
||||
optional<network::IPAddress> listen_address_{};
|
||||
};
|
||||
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
} // namespace esphome::udp
|
||||
#endif
|
||||
|
||||
@@ -143,7 +143,7 @@ bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) {
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
bool ListEntitiesIterator::on_infrared(infrared::Infrared *obj) {
|
||||
// Infrared web_server support not yet implemented - this stub acknowledges the entity
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::infrared_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER_LOCAL
|
||||
#if USE_WEBSERVER_VERSION == 2
|
||||
#include "server_index_v2.h"
|
||||
@@ -658,6 +662,24 @@ std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
enum SwitchAction : uint8_t { SWITCH_ACTION_NONE, SWITCH_ACTION_TOGGLE, SWITCH_ACTION_TURN_ON, SWITCH_ACTION_TURN_OFF };
|
||||
|
||||
static void execute_switch_action(switch_::Switch *obj, SwitchAction action) {
|
||||
switch (action) {
|
||||
case SWITCH_ACTION_TOGGLE:
|
||||
obj->toggle();
|
||||
break;
|
||||
case SWITCH_ACTION_TURN_ON:
|
||||
obj->turn_on();
|
||||
break;
|
||||
case SWITCH_ACTION_TURN_OFF:
|
||||
obj->turn_off();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WebServer::on_switch_update(switch_::Switch *obj) {
|
||||
if (!this->include_internal_ && obj->is_internal())
|
||||
return;
|
||||
@@ -676,34 +698,22 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle action methods with single defer and response
|
||||
enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF };
|
||||
SwitchAction action = NONE;
|
||||
SwitchAction action = SWITCH_ACTION_NONE;
|
||||
|
||||
if (match.method_equals(ESPHOME_F("toggle"))) {
|
||||
action = TOGGLE;
|
||||
action = SWITCH_ACTION_TOGGLE;
|
||||
} else if (match.method_equals(ESPHOME_F("turn_on"))) {
|
||||
action = TURN_ON;
|
||||
action = SWITCH_ACTION_TURN_ON;
|
||||
} else if (match.method_equals(ESPHOME_F("turn_off"))) {
|
||||
action = TURN_OFF;
|
||||
action = SWITCH_ACTION_TURN_OFF;
|
||||
}
|
||||
|
||||
if (action != NONE) {
|
||||
this->defer([obj, action]() {
|
||||
switch (action) {
|
||||
case TOGGLE:
|
||||
obj->toggle();
|
||||
break;
|
||||
case TURN_ON:
|
||||
obj->turn_on();
|
||||
break;
|
||||
case TURN_OFF:
|
||||
obj->turn_off();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (action != SWITCH_ACTION_NONE) {
|
||||
#ifdef USE_ESP8266
|
||||
execute_switch_action(obj, action);
|
||||
#else
|
||||
this->defer([obj, action]() { execute_switch_action(obj, action); });
|
||||
#endif
|
||||
request->send(200);
|
||||
} else {
|
||||
request->send(404);
|
||||
@@ -743,7 +753,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
|
||||
std::string data = this->button_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method_equals(ESPHOME_F("press"))) {
|
||||
this->defer([obj]() { obj->press(); });
|
||||
DEFER_ACTION(obj, obj->press());
|
||||
request->send(200);
|
||||
return;
|
||||
} else {
|
||||
@@ -828,7 +838,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
std::string data = this->fan_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method_equals(ESPHOME_F("toggle"))) {
|
||||
this->defer([obj]() { obj->toggle().perform(); });
|
||||
DEFER_ACTION(obj, obj->toggle().perform());
|
||||
request->send(200);
|
||||
} else {
|
||||
bool is_on = match.method_equals(ESPHOME_F("turn_on"));
|
||||
@@ -859,7 +869,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
}
|
||||
return;
|
||||
@@ -909,7 +919,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
std::string data = this->light_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
} else if (match.method_equals(ESPHOME_F("toggle"))) {
|
||||
this->defer([obj]() { obj->toggle().perform(); });
|
||||
DEFER_ACTION(obj, obj->toggle().perform());
|
||||
request->send(200);
|
||||
} else {
|
||||
bool is_on = match.method_equals(ESPHOME_F("turn_on"));
|
||||
@@ -938,7 +948,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect);
|
||||
}
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
}
|
||||
return;
|
||||
@@ -1027,7 +1037,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1086,7 +1096,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
auto call = obj->make_call();
|
||||
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1159,7 +1169,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1223,7 +1233,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1286,7 +1296,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1346,7 +1356,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
auto call = obj->make_call();
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1404,7 +1414,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
auto call = obj->make_call();
|
||||
parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1473,7 +1483,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1589,6 +1599,24 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOCK
|
||||
enum LockAction : uint8_t { LOCK_ACTION_NONE, LOCK_ACTION_LOCK, LOCK_ACTION_UNLOCK, LOCK_ACTION_OPEN };
|
||||
|
||||
static void execute_lock_action(lock::Lock *obj, LockAction action) {
|
||||
switch (action) {
|
||||
case LOCK_ACTION_LOCK:
|
||||
obj->lock();
|
||||
break;
|
||||
case LOCK_ACTION_UNLOCK:
|
||||
obj->unlock();
|
||||
break;
|
||||
case LOCK_ACTION_OPEN:
|
||||
obj->open();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WebServer::on_lock_update(lock::Lock *obj) {
|
||||
if (!this->include_internal_ && obj->is_internal())
|
||||
return;
|
||||
@@ -1607,34 +1635,22 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle action methods with single defer and response
|
||||
enum LockAction { NONE, LOCK, UNLOCK, OPEN };
|
||||
LockAction action = NONE;
|
||||
LockAction action = LOCK_ACTION_NONE;
|
||||
|
||||
if (match.method_equals(ESPHOME_F("lock"))) {
|
||||
action = LOCK;
|
||||
action = LOCK_ACTION_LOCK;
|
||||
} else if (match.method_equals(ESPHOME_F("unlock"))) {
|
||||
action = UNLOCK;
|
||||
action = LOCK_ACTION_UNLOCK;
|
||||
} else if (match.method_equals(ESPHOME_F("open"))) {
|
||||
action = OPEN;
|
||||
action = LOCK_ACTION_OPEN;
|
||||
}
|
||||
|
||||
if (action != NONE) {
|
||||
this->defer([obj, action]() {
|
||||
switch (action) {
|
||||
case LOCK:
|
||||
obj->lock();
|
||||
break;
|
||||
case UNLOCK:
|
||||
obj->unlock();
|
||||
break;
|
||||
case OPEN:
|
||||
obj->open();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (action != LOCK_ACTION_NONE) {
|
||||
#ifdef USE_ESP8266
|
||||
execute_lock_action(obj, action);
|
||||
#else
|
||||
this->defer([obj, action]() { execute_lock_action(obj, action); });
|
||||
#endif
|
||||
request->send(200);
|
||||
} else {
|
||||
request->send(404);
|
||||
@@ -1717,7 +1733,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
|
||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1796,7 +1812,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
return;
|
||||
}
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1872,7 +1888,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
// Parse on/off parameter
|
||||
parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -1940,6 +1956,110 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (infrared::Infrared *obj : App.get_infrareds()) {
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->infrared_json_(obj, detail);
|
||||
request->send(200, ESPHOME_F("application/json"), data.c_str());
|
||||
return;
|
||||
}
|
||||
if (!match.method_equals(ESPHOME_F("transmit"))) {
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow transmit if the device supports it
|
||||
if (!obj->has_transmitter()) {
|
||||
request->send(400, ESPHOME_F("text/plain"), "Device does not support transmission");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse parameters
|
||||
auto call = obj->make_call();
|
||||
|
||||
// Parse carrier frequency (optional)
|
||||
if (request->hasParam(ESPHOME_F("carrier_frequency"))) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
|
||||
if (value.has_value()) {
|
||||
call.set_carrier_frequency(*value);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse repeat count (optional, defaults to 1)
|
||||
if (request->hasParam(ESPHOME_F("repeat_count"))) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
|
||||
if (value.has_value()) {
|
||||
call.set_repeat_count(*value);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse base64url-encoded raw timings (required)
|
||||
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
|
||||
if (!request->hasParam(ESPHOME_F("data"))) {
|
||||
request->send(400, ESPHOME_F("text/plain"), "Missing 'data' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
||||
std::string encoded =
|
||||
request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
|
||||
// Validate base64url is not empty
|
||||
if (encoded.empty()) {
|
||||
request->send(400, ESPHOME_F("text/plain"), "Empty 'data' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266 is single-threaded, call directly
|
||||
call.set_raw_timings_base64url(encoded);
|
||||
call.perform();
|
||||
#else
|
||||
// Defer to main loop for thread safety. Move encoded string into lambda to ensure
|
||||
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
|
||||
// must remain valid until perform() completes.
|
||||
this->defer([call, encoded = std::move(encoded)]() mutable {
|
||||
call.set_raw_timings_base64url(encoded);
|
||||
call.perform();
|
||||
});
|
||||
#endif
|
||||
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
|
||||
}
|
||||
|
||||
std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_icon_state_value(root, obj, "infrared", "", 0, start_config);
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
|
||||
root[ESPHOME_F("supports_transmitter")] = traits.get_supports_transmitter();
|
||||
root[ESPHOME_F("supports_receiver")] = traits.get_supports_receiver();
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void WebServer::on_event(event::Event *obj) {
|
||||
if (!this->include_internal_ && obj->is_internal())
|
||||
@@ -2032,7 +2152,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
|
||||
return;
|
||||
}
|
||||
|
||||
this->defer([obj]() mutable { obj->perform(); });
|
||||
DEFER_ACTION(obj, obj->perform());
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
@@ -2071,24 +2191,21 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
||||
const auto &url = request->url();
|
||||
const auto method = request->method();
|
||||
|
||||
// Static URL checks
|
||||
static const char *const STATIC_URLS[] = {
|
||||
"/",
|
||||
// Static URL checks - use ESPHOME_F to keep strings in flash on ESP8266
|
||||
if (url == ESPHOME_F("/"))
|
||||
return true;
|
||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||
"/events",
|
||||
if (url == ESPHOME_F("/events"))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||
"/0.css",
|
||||
if (url == ESPHOME_F("/0.css"))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||
"/0.js",
|
||||
if (url == ESPHOME_F("/0.js"))
|
||||
return true;
|
||||
#endif
|
||||
};
|
||||
|
||||
for (const auto &static_url : STATIC_URLS) {
|
||||
if (url == static_url)
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
|
||||
if (method == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network")))
|
||||
@@ -2108,90 +2225,100 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
||||
if (!is_get_or_post)
|
||||
return false;
|
||||
|
||||
// Use lookup tables for domain checks
|
||||
static const char *const GET_ONLY_DOMAINS[] = {
|
||||
// Check GET-only domains - use ESPHOME_F to keep strings in flash on ESP8266
|
||||
if (is_get) {
|
||||
#ifdef USE_SENSOR
|
||||
"sensor",
|
||||
if (match.domain_equals(ESPHOME_F("sensor")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
"binary_sensor",
|
||||
if (match.domain_equals(ESPHOME_F("binary_sensor")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
"text_sensor",
|
||||
if (match.domain_equals(ESPHOME_F("text_sensor")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
"event",
|
||||
if (match.domain_equals(ESPHOME_F("event")))
|
||||
return true;
|
||||
#endif
|
||||
};
|
||||
|
||||
static const char *const GET_POST_DOMAINS[] = {
|
||||
#ifdef USE_SWITCH
|
||||
"switch",
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
"button",
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
"fan",
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
"light",
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
"cover",
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
"number",
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
"date",
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
"time",
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
"datetime",
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
"text",
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
"select",
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
"climate",
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
"lock",
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
"valve",
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
"alarm_control_panel",
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
"update",
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
"water_heater",
|
||||
#endif
|
||||
};
|
||||
|
||||
// Check GET-only domains
|
||||
if (is_get) {
|
||||
for (const auto &domain : GET_ONLY_DOMAINS) {
|
||||
if (match.domain_equals(domain))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check GET+POST domains
|
||||
if (is_get_or_post) {
|
||||
for (const auto &domain : GET_POST_DOMAINS) {
|
||||
if (match.domain_equals(domain))
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
if (match.domain_equals(ESPHOME_F("switch")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
if (match.domain_equals(ESPHOME_F("button")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
if (match.domain_equals(ESPHOME_F("fan")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
if (match.domain_equals(ESPHOME_F("light")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
if (match.domain_equals(ESPHOME_F("cover")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
if (match.domain_equals(ESPHOME_F("number")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
if (match.domain_equals(ESPHOME_F("date")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
if (match.domain_equals(ESPHOME_F("time")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
if (match.domain_equals(ESPHOME_F("datetime")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
if (match.domain_equals(ESPHOME_F("text")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (match.domain_equals(ESPHOME_F("select")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
if (match.domain_equals(ESPHOME_F("climate")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
if (match.domain_equals(ESPHOME_F("lock")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
if (match.domain_equals(ESPHOME_F("valve")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
if (match.domain_equals(ESPHOME_F("alarm_control_panel")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
if (match.domain_equals(ESPHOME_F("update")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
if (match.domain_equals(ESPHOME_F("water_heater")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
if (match.domain_equals(ESPHOME_F("infrared")))
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -2340,6 +2467,11 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
||||
else if (match.domain_equals(ESPHOME_F("water_heater"))) {
|
||||
this->handle_water_heater_request(request, match);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
else if (match.domain_equals(ESPHOME_F("infrared"))) {
|
||||
this->handle_infrared_request(request, match);
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
// No matching handler found - send 404
|
||||
|
||||
@@ -42,6 +42,14 @@ using ParamNameType = const __FlashStringHelper *;
|
||||
using ParamNameType = const char *;
|
||||
#endif
|
||||
|
||||
// ESP8266 is single-threaded, so actions can execute directly in request context.
|
||||
// Multi-core platforms need to defer to main loop thread for thread safety.
|
||||
#ifdef USE_ESP8266
|
||||
#define DEFER_ACTION(capture, action) action
|
||||
#else
|
||||
#define DEFER_ACTION(capture, action) this->defer([capture]() mutable { action; })
|
||||
#endif
|
||||
|
||||
/// Result of matching a URL against an entity
|
||||
struct EntityMatchResult {
|
||||
bool matched; ///< True if entity matched the URL
|
||||
@@ -452,6 +460,13 @@ class WebServer : public Controller,
|
||||
static std::string water_heater_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
/// Handle an infrared request under '/infrared/<id>/transmit'.
|
||||
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static std::string infrared_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj) override;
|
||||
|
||||
@@ -654,6 +669,9 @@ class WebServer : public Controller,
|
||||
#ifdef USE_WATER_HEATER
|
||||
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
|
||||
@@ -4,19 +4,13 @@
|
||||
/// @details The classes declared in this file can be used by the Weikai family
|
||||
|
||||
#include "weikai.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace weikai {
|
||||
|
||||
static const char *const TAG = "weikai";
|
||||
|
||||
/// @brief convert an int to binary representation as C++ std::string
|
||||
/// @param val integer to convert
|
||||
/// @return a std::string
|
||||
inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); }
|
||||
/// Convert std::string to C string
|
||||
#define I2S2CS(val) (i2s(val).c_str())
|
||||
|
||||
/// @brief measure the time elapsed between two calls
|
||||
/// @param last_time time of the previous call
|
||||
/// @return the elapsed time in milliseconds
|
||||
@@ -170,17 +164,18 @@ void WeikaiComponent::test_gpio_input_() {
|
||||
static bool init_input{false};
|
||||
static uint8_t state{0};
|
||||
uint8_t value;
|
||||
char bin_buf[9]; // 8 binary digits + null
|
||||
if (!init_input) {
|
||||
init_input = true;
|
||||
// set all pins in input mode
|
||||
this->reg(WKREG_GPDIR, 0) = 0x00;
|
||||
ESP_LOGI(TAG, "initializing all pins to input mode");
|
||||
state = this->reg(WKREG_GPDAT, 0);
|
||||
ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, I2S2CS(state));
|
||||
ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, format_bin_to(bin_buf, state));
|
||||
}
|
||||
value = this->reg(WKREG_GPDAT, 0);
|
||||
if (value != state) {
|
||||
ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, I2S2CS(value));
|
||||
ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, format_bin_to(bin_buf, value));
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
@@ -188,6 +183,7 @@ void WeikaiComponent::test_gpio_input_() {
|
||||
void WeikaiComponent::test_gpio_output_() {
|
||||
static bool init_output{false};
|
||||
static uint8_t state{0};
|
||||
char bin_buf[9]; // 8 binary digits + null
|
||||
if (!init_output) {
|
||||
init_output = true;
|
||||
// set all pins in output mode
|
||||
@@ -198,7 +194,7 @@ void WeikaiComponent::test_gpio_output_() {
|
||||
}
|
||||
state = ~state;
|
||||
this->reg(WKREG_GPDAT, 0) = state;
|
||||
ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, I2S2CS(state));
|
||||
ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, format_bin_to(bin_buf, state));
|
||||
delay(100); // NOLINT
|
||||
}
|
||||
#endif
|
||||
@@ -208,7 +204,9 @@ void WeikaiComponent::test_gpio_output_() {
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
bool WeikaiComponent::read_pin_val_(uint8_t pin) {
|
||||
this->input_state_ = this->reg(WKREG_GPDAT, 0);
|
||||
ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin), I2S2CS(input_state_));
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin),
|
||||
format_bin_to(bin_buf, this->input_state_));
|
||||
return this->input_state_ & (1 << pin);
|
||||
}
|
||||
|
||||
@@ -218,7 +216,9 @@ void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
|
||||
} else {
|
||||
this->output_state_ &= ~(1 << pin);
|
||||
}
|
||||
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value), I2S2CS(this->output_state_));
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
|
||||
format_bin_to(bin_buf, this->output_state_));
|
||||
this->reg(WKREG_GPDAT, 0) = this->output_state_;
|
||||
}
|
||||
|
||||
@@ -232,7 +232,8 @@ void WeikaiComponent::set_pin_direction_(uint8_t pin, gpio::Flags flags) {
|
||||
ESP_LOGE(TAG, "pin %d direction invalid", pin);
|
||||
}
|
||||
}
|
||||
ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, I2S2CS(this->pin_config_));
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, format_bin_to(bin_buf, this->pin_config_));
|
||||
this->reg(WKREG_GPDIR, 0) = this->pin_config_; // TODO check ~
|
||||
}
|
||||
|
||||
@@ -241,7 +242,6 @@ void WeikaiGPIOPin::setup() {
|
||||
flags_ == gpio::FLAG_INPUT ? "Input"
|
||||
: this->flags_ == gpio::FLAG_OUTPUT ? "Output"
|
||||
: "NOT SPECIFIED");
|
||||
// ESP_LOGCONFIG(TAG, "Setting GPIO pins mode to '%s' %02X", I2S2CS(this->flags_), this->flags_);
|
||||
this->pin_mode(this->flags_);
|
||||
}
|
||||
|
||||
@@ -297,8 +297,9 @@ void WeikaiChannel::set_line_param_() {
|
||||
break; // no parity 000x
|
||||
}
|
||||
this->reg(WKREG_LCR) = lcr; // write LCR
|
||||
char bin_buf[9];
|
||||
ESP_LOGV(TAG, " line config: %d data_bits, %d stop_bits, parity %s register [%s]", this->data_bits_,
|
||||
this->stop_bits_, p2s(this->parity_), I2S2CS(lcr));
|
||||
this->stop_bits_, p2s(this->parity_), format_bin_to(bin_buf, lcr));
|
||||
}
|
||||
|
||||
void WeikaiChannel::set_baudrate_() {
|
||||
@@ -334,7 +335,8 @@ size_t WeikaiChannel::tx_in_fifo_() {
|
||||
if (tfcnt == 0) {
|
||||
uint8_t const fsr = this->reg(WKREG_FSR);
|
||||
if (fsr & FSR_TFFULL) {
|
||||
ESP_LOGVV(TAG, "tx FIFO full FSR=%s", I2S2CS(fsr));
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "tx FIFO full FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
tfcnt = FIFO_SIZE;
|
||||
}
|
||||
}
|
||||
@@ -346,14 +348,15 @@ size_t WeikaiChannel::rx_in_fifo_() {
|
||||
size_t available = this->reg(WKREG_RFCNT);
|
||||
uint8_t const fsr = this->reg(WKREG_FSR);
|
||||
if (fsr & (FSR_RFOE | FSR_RFLB | FSR_RFFE | FSR_RFPE)) {
|
||||
char bin_buf[9];
|
||||
if (fsr & FSR_RFOE)
|
||||
ESP_LOGE(TAG, "Receive data overflow FSR=%s", I2S2CS(fsr));
|
||||
ESP_LOGE(TAG, "Receive data overflow FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFLB)
|
||||
ESP_LOGE(TAG, "Receive line break FSR=%s", I2S2CS(fsr));
|
||||
ESP_LOGE(TAG, "Receive line break FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFFE)
|
||||
ESP_LOGE(TAG, "Receive frame error FSR=%s", I2S2CS(fsr));
|
||||
ESP_LOGE(TAG, "Receive frame error FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFPE)
|
||||
ESP_LOGE(TAG, "Receive parity error FSR=%s", I2S2CS(fsr));
|
||||
ESP_LOGE(TAG, "Receive parity error FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
}
|
||||
if ((available == 0) && (fsr & FSR_RFDAT)) {
|
||||
// here we should be very careful because we can have something like this:
|
||||
@@ -362,11 +365,13 @@ size_t WeikaiChannel::rx_in_fifo_() {
|
||||
// - so to be sure we need to do another read of RFCNT and if it is still zero -> buffer full
|
||||
available = this->reg(WKREG_RFCNT);
|
||||
if (available == 0) { // still zero ?
|
||||
ESP_LOGV(TAG, "rx FIFO is full FSR=%s", I2S2CS(fsr));
|
||||
char bin_buf[9];
|
||||
ESP_LOGV(TAG, "rx FIFO is full FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
available = FIFO_SIZE;
|
||||
}
|
||||
}
|
||||
ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, I2S2CS(fsr));
|
||||
char bin_buf2[9];
|
||||
ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, format_bin_to(bin_buf2, fsr));
|
||||
return available;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
/// wk2132_i2c, wk2168_i2c, wk2204_i2c, wk2212_i2c
|
||||
|
||||
#pragma once
|
||||
#include <bitset>
|
||||
#include <memory>
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
@@ -10,13 +10,6 @@ namespace weikai_spi {
|
||||
using namespace weikai;
|
||||
static const char *const TAG = "weikai_spi";
|
||||
|
||||
/// @brief convert an int to binary representation as C++ std::string
|
||||
/// @param val integer to convert
|
||||
/// @return a std::string
|
||||
inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); }
|
||||
/// Convert std::string to C string
|
||||
#define I2S2CS(val) (i2s(val).c_str())
|
||||
|
||||
/// @brief measure the time elapsed between two calls
|
||||
/// @param last_time time of the previous call
|
||||
/// @return the elapsed time in microseconds
|
||||
@@ -107,7 +100,8 @@ uint8_t WeikaiRegisterSPI::read_reg() const {
|
||||
spi_comp->write_byte(cmd);
|
||||
uint8_t val = spi_comp->read_byte();
|
||||
spi_comp->disable();
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", I2S2CS(cmd), cmd,
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, cmd), cmd,
|
||||
reg_to_str(this->register_, this->comp_->page1()), this->channel_, val);
|
||||
return val;
|
||||
}
|
||||
@@ -120,8 +114,9 @@ void WeikaiRegisterSPI::read_fifo(uint8_t *data, size_t length) const {
|
||||
spi_comp->read_array(data, length);
|
||||
spi_comp->disable();
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", I2S2CS(cmd), cmd, this->channel_,
|
||||
length);
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
|
||||
this->channel_, length);
|
||||
print_buffer(data, length);
|
||||
#endif
|
||||
}
|
||||
@@ -132,8 +127,9 @@ void WeikaiRegisterSPI::write_reg(uint8_t value) {
|
||||
spi_comp->enable();
|
||||
spi_comp->write_array(buf, 2);
|
||||
spi_comp->disable();
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", I2S2CS(buf[0]), buf[0],
|
||||
reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, buf[0]),
|
||||
buf[0], reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
|
||||
}
|
||||
|
||||
void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
@@ -145,8 +141,9 @@ void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
spi_comp->disable();
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", I2S2CS(cmd), cmd, this->channel_,
|
||||
length);
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
|
||||
this->channel_, length);
|
||||
print_buffer(data, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
/// wk2124_spi, wk2132_spi, wk2168_spi, wk2204_spi, wk2212_spi,
|
||||
|
||||
#pragma once
|
||||
#include <bitset>
|
||||
#include <memory>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
@@ -30,6 +30,7 @@ _WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$")
|
||||
|
||||
wireguard_ns = cg.esphome_ns.namespace("wireguard")
|
||||
Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent)
|
||||
AllowedIP = wireguard_ns.struct("AllowedIP")
|
||||
WireguardPeerOnlineCondition = wireguard_ns.class_(
|
||||
"WireguardPeerOnlineCondition", automation.Condition
|
||||
)
|
||||
@@ -108,8 +109,18 @@ async def to_code(config):
|
||||
)
|
||||
)
|
||||
|
||||
for ip in allowed_ips:
|
||||
cg.add(var.add_allowed_ip(str(ip.network_address), str(ip.netmask)))
|
||||
cg.add(
|
||||
var.set_allowed_ips(
|
||||
[
|
||||
cg.StructInitializer(
|
||||
AllowedIP,
|
||||
("ip", str(ip.network_address)),
|
||||
("netmask", str(ip.netmask)),
|
||||
)
|
||||
for ip in allowed_ips
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
cg.add(var.set_srctime(await cg.get_variable(config[CONF_TIME_ID])))
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
#include <esp_wireguard.h>
|
||||
#include <esp_wireguard_err.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace wireguard {
|
||||
namespace esphome::wireguard {
|
||||
|
||||
static const char *const TAG = "wireguard";
|
||||
|
||||
@@ -28,16 +27,16 @@ static const char *const LOGMSG_ONLINE = "online";
|
||||
static const char *const LOGMSG_OFFLINE = "offline";
|
||||
|
||||
void Wireguard::setup() {
|
||||
this->wg_config_.address = this->address_.c_str();
|
||||
this->wg_config_.private_key = this->private_key_.c_str();
|
||||
this->wg_config_.endpoint = this->peer_endpoint_.c_str();
|
||||
this->wg_config_.public_key = this->peer_public_key_.c_str();
|
||||
this->wg_config_.address = this->address_;
|
||||
this->wg_config_.private_key = this->private_key_;
|
||||
this->wg_config_.endpoint = this->peer_endpoint_;
|
||||
this->wg_config_.public_key = this->peer_public_key_;
|
||||
this->wg_config_.port = this->peer_port_;
|
||||
this->wg_config_.netmask = this->netmask_.c_str();
|
||||
this->wg_config_.netmask = this->netmask_;
|
||||
this->wg_config_.persistent_keepalive = this->keepalive_;
|
||||
|
||||
if (!this->preshared_key_.empty())
|
||||
this->wg_config_.preshared_key = this->preshared_key_.c_str();
|
||||
if (this->preshared_key_ != nullptr)
|
||||
this->wg_config_.preshared_key = this->preshared_key_;
|
||||
|
||||
this->publish_enabled_state();
|
||||
|
||||
@@ -131,6 +130,10 @@ void Wireguard::update() {
|
||||
}
|
||||
|
||||
void Wireguard::dump_config() {
|
||||
char private_key_masked[MASK_KEY_BUFFER_SIZE];
|
||||
char preshared_key_masked[MASK_KEY_BUFFER_SIZE];
|
||||
mask_key_to(private_key_masked, sizeof(private_key_masked), this->private_key_);
|
||||
mask_key_to(preshared_key_masked, sizeof(preshared_key_masked), this->preshared_key_);
|
||||
// clang-format off
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
@@ -142,13 +145,13 @@ void Wireguard::dump_config() {
|
||||
" Peer Port: " LOG_SECRET("%d") "\n"
|
||||
" Peer Public Key: " LOG_SECRET("%s") "\n"
|
||||
" Peer Pre-shared Key: " LOG_SECRET("%s"),
|
||||
this->address_.c_str(), this->netmask_.c_str(), mask_key(this->private_key_).c_str(),
|
||||
this->peer_endpoint_.c_str(), this->peer_port_, this->peer_public_key_.c_str(),
|
||||
(!this->preshared_key_.empty() ? mask_key(this->preshared_key_).c_str() : "NOT IN USE"));
|
||||
this->address_, this->netmask_, private_key_masked,
|
||||
this->peer_endpoint_, this->peer_port_, this->peer_public_key_,
|
||||
(this->preshared_key_ != nullptr ? preshared_key_masked : "NOT IN USE"));
|
||||
// clang-format on
|
||||
ESP_LOGCONFIG(TAG, " Peer Allowed IPs:");
|
||||
for (auto &allowed_ip : this->allowed_ips_) {
|
||||
ESP_LOGCONFIG(TAG, " - %s/%s", std::get<0>(allowed_ip).c_str(), std::get<1>(allowed_ip).c_str());
|
||||
for (const AllowedIP &allowed_ip : this->allowed_ips_) {
|
||||
ESP_LOGCONFIG(TAG, " - %s/%s", allowed_ip.ip, allowed_ip.netmask);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Peer Persistent Keepalive: %d%s", this->keepalive_,
|
||||
(this->keepalive_ > 0 ? "s" : " (DISABLED)"));
|
||||
@@ -176,18 +179,6 @@ time_t Wireguard::get_latest_handshake() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
void Wireguard::set_address(const std::string &address) { this->address_ = address; }
|
||||
void Wireguard::set_netmask(const std::string &netmask) { this->netmask_ = netmask; }
|
||||
void Wireguard::set_private_key(const std::string &key) { this->private_key_ = key; }
|
||||
void Wireguard::set_peer_endpoint(const std::string &endpoint) { this->peer_endpoint_ = endpoint; }
|
||||
void Wireguard::set_peer_public_key(const std::string &key) { this->peer_public_key_ = key; }
|
||||
void Wireguard::set_peer_port(const uint16_t port) { this->peer_port_ = port; }
|
||||
void Wireguard::set_preshared_key(const std::string &key) { this->preshared_key_ = key; }
|
||||
|
||||
void Wireguard::add_allowed_ip(const std::string &ip, const std::string &netmask) {
|
||||
this->allowed_ips_.emplace_back(ip, netmask);
|
||||
}
|
||||
|
||||
void Wireguard::set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; }
|
||||
void Wireguard::set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; }
|
||||
void Wireguard::set_srctime(time::RealTimeClock *srctime) { this->srctime_ = srctime; }
|
||||
@@ -274,9 +265,8 @@ void Wireguard::start_connection_() {
|
||||
|
||||
ESP_LOGD(TAG, "Configuring allowed IPs list");
|
||||
bool allowed_ips_ok = true;
|
||||
for (std::tuple<std::string, std::string> ip : this->allowed_ips_) {
|
||||
allowed_ips_ok &=
|
||||
(esp_wireguard_add_allowed_ip(&(this->wg_ctx_), std::get<0>(ip).c_str(), std::get<1>(ip).c_str()) == ESP_OK);
|
||||
for (const AllowedIP &ip : this->allowed_ips_) {
|
||||
allowed_ips_ok &= (esp_wireguard_add_allowed_ip(&(this->wg_ctx_), ip.ip, ip.netmask) == ESP_OK);
|
||||
}
|
||||
|
||||
if (allowed_ips_ok) {
|
||||
@@ -299,8 +289,25 @@ void Wireguard::stop_connection_() {
|
||||
}
|
||||
}
|
||||
|
||||
std::string mask_key(const std::string &key) { return (key.substr(0, 5) + "[...]="); }
|
||||
void mask_key_to(char *buffer, size_t len, const char *key) {
|
||||
// Format: "XXXXX[...]=\0" = MASK_KEY_BUFFER_SIZE chars minimum
|
||||
if (len < MASK_KEY_BUFFER_SIZE || key == nullptr) {
|
||||
if (len > 0)
|
||||
buffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
// Copy first 5 characters of the key
|
||||
size_t i = 0;
|
||||
for (; i < 5 && key[i] != '\0'; ++i) {
|
||||
buffer[i] = key[i];
|
||||
}
|
||||
// Append "[...]="
|
||||
const char *suffix = "[...]=";
|
||||
for (size_t j = 0; suffix[j] != '\0' && (i + j) < len - 1; ++j) {
|
||||
buffer[i + j] = suffix[j];
|
||||
}
|
||||
buffer[i + 6] = '\0';
|
||||
}
|
||||
|
||||
} // namespace wireguard
|
||||
} // namespace esphome
|
||||
} // namespace esphome::wireguard
|
||||
#endif
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_WIREGUARD
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -22,8 +22,13 @@
|
||||
|
||||
#include <esp_wireguard.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace wireguard {
|
||||
namespace esphome::wireguard {
|
||||
|
||||
/// Allowed IP entry for WireGuard peer configuration.
|
||||
struct AllowedIP {
|
||||
const char *ip;
|
||||
const char *netmask;
|
||||
};
|
||||
|
||||
/// Main Wireguard component class.
|
||||
class Wireguard : public PollingComponent {
|
||||
@@ -37,15 +42,25 @@ class Wireguard : public PollingComponent {
|
||||
|
||||
float get_setup_priority() const override { return esphome::setup_priority::BEFORE_CONNECTION; }
|
||||
|
||||
void set_address(const std::string &address);
|
||||
void set_netmask(const std::string &netmask);
|
||||
void set_private_key(const std::string &key);
|
||||
void set_peer_endpoint(const std::string &endpoint);
|
||||
void set_peer_public_key(const std::string &key);
|
||||
void set_peer_port(uint16_t port);
|
||||
void set_preshared_key(const std::string &key);
|
||||
void set_address(const char *address) { this->address_ = address; }
|
||||
void set_netmask(const char *netmask) { this->netmask_ = netmask; }
|
||||
void set_private_key(const char *key) { this->private_key_ = key; }
|
||||
void set_peer_endpoint(const char *endpoint) { this->peer_endpoint_ = endpoint; }
|
||||
void set_peer_public_key(const char *key) { this->peer_public_key_ = key; }
|
||||
void set_peer_port(uint16_t port) { this->peer_port_ = port; }
|
||||
void set_preshared_key(const char *key) { this->preshared_key_ = key; }
|
||||
|
||||
void add_allowed_ip(const std::string &ip, const std::string &netmask);
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_address(const std::string &address) = delete;
|
||||
void set_netmask(const std::string &netmask) = delete;
|
||||
void set_private_key(const std::string &key) = delete;
|
||||
void set_peer_endpoint(const std::string &endpoint) = delete;
|
||||
void set_peer_public_key(const std::string &key) = delete;
|
||||
void set_preshared_key(const std::string &key) = delete;
|
||||
|
||||
void set_allowed_ips(std::initializer_list<AllowedIP> ips) { this->allowed_ips_ = ips; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_allowed_ips(std::initializer_list<std::tuple<std::string, std::string>> ips) = delete;
|
||||
|
||||
void set_keepalive(uint16_t seconds);
|
||||
void set_reboot_timeout(uint32_t seconds);
|
||||
@@ -83,14 +98,14 @@ class Wireguard : public PollingComponent {
|
||||
time_t get_latest_handshake() const;
|
||||
|
||||
protected:
|
||||
std::string address_;
|
||||
std::string netmask_;
|
||||
std::string private_key_;
|
||||
std::string peer_endpoint_;
|
||||
std::string peer_public_key_;
|
||||
std::string preshared_key_;
|
||||
const char *address_{nullptr};
|
||||
const char *netmask_{nullptr};
|
||||
const char *private_key_{nullptr};
|
||||
const char *peer_endpoint_{nullptr};
|
||||
const char *peer_public_key_{nullptr};
|
||||
const char *preshared_key_{nullptr};
|
||||
|
||||
std::vector<std::tuple<std::string, std::string>> allowed_ips_;
|
||||
FixedVector<AllowedIP> allowed_ips_;
|
||||
|
||||
uint16_t peer_port_;
|
||||
uint16_t keepalive_;
|
||||
@@ -142,8 +157,11 @@ class Wireguard : public PollingComponent {
|
||||
void suspend_wdt();
|
||||
void resume_wdt();
|
||||
|
||||
/// Size of buffer required for mask_key_to: 5 chars + "[...]=" + null = 12
|
||||
static constexpr size_t MASK_KEY_BUFFER_SIZE = 12;
|
||||
|
||||
/// Strip most part of the key only for secure printing
|
||||
std::string mask_key(const std::string &key);
|
||||
void mask_key_to(char *buffer, size_t len, const char *key);
|
||||
|
||||
/// Condition to check if remote peer is online.
|
||||
template<typename... Ts> class WireguardPeerOnlineCondition : public Condition<Ts...>, public Parented<Wireguard> {
|
||||
@@ -169,6 +187,5 @@ template<typename... Ts> class WireguardDisableAction : public Action<Ts...>, pu
|
||||
void play(const Ts &...x) override { this->parent_->disable(); }
|
||||
};
|
||||
|
||||
} // namespace wireguard
|
||||
} // namespace esphome
|
||||
} // namespace esphome::wireguard
|
||||
#endif
|
||||
|
||||
@@ -404,15 +404,31 @@ std::string format_hex_pretty(const std::string &data, char separator, bool show
|
||||
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
|
||||
}
|
||||
|
||||
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
|
||||
if (buffer_size == 0) {
|
||||
return buffer;
|
||||
}
|
||||
// Calculate max bytes we can format: each byte needs 8 chars
|
||||
size_t max_bytes = (buffer_size - 1) / 8;
|
||||
if (max_bytes == 0 || length == 0) {
|
||||
buffer[0] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
size_t bytes_to_format = std::min(length, max_bytes);
|
||||
|
||||
for (size_t byte_idx = 0; byte_idx < bytes_to_format; byte_idx++) {
|
||||
for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
|
||||
buffer[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
|
||||
}
|
||||
}
|
||||
buffer[bytes_to_format * 8] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string format_bin(const uint8_t *data, size_t length) {
|
||||
std::string result;
|
||||
result.resize(length * 8);
|
||||
for (size_t byte_idx = 0; byte_idx < length; byte_idx++) {
|
||||
for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
|
||||
result[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
|
||||
}
|
||||
}
|
||||
|
||||
format_bin_to(&result[0], length * 8 + 1, data, length);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -624,53 +640,44 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Encode int32 to 5 base85 characters + null terminator
|
||||
/// Standard ASCII85 alphabet: '!' (33) = 0 through 'u' (117) = 84
|
||||
inline void base85_encode_int32(int32_t value, std::span<char, BASE85_INT32_ENCODED_SIZE> output) {
|
||||
uint32_t v = static_cast<uint32_t>(value);
|
||||
// Encode least significant digit first, then reverse
|
||||
for (int i = 4; i >= 0; i--) {
|
||||
output[i] = static_cast<char>('!' + (v % 85));
|
||||
v /= 85;
|
||||
}
|
||||
output[5] = '\0';
|
||||
}
|
||||
|
||||
/// Decode 5 base85 characters to int32
|
||||
inline bool base85_decode_int32(const char *input, int32_t &out) {
|
||||
uint8_t c0 = static_cast<uint8_t>(input[0] - '!');
|
||||
uint8_t c1 = static_cast<uint8_t>(input[1] - '!');
|
||||
uint8_t c2 = static_cast<uint8_t>(input[2] - '!');
|
||||
uint8_t c3 = static_cast<uint8_t>(input[3] - '!');
|
||||
uint8_t c4 = static_cast<uint8_t>(input[4] - '!');
|
||||
|
||||
// Each digit must be 0-84. Since uint8_t wraps, chars below '!' become > 84
|
||||
if (c0 > 84 || c1 > 84 || c2 > 84 || c3 > 84 || c4 > 84)
|
||||
return false;
|
||||
|
||||
// 85^4 = 52200625, 85^3 = 614125, 85^2 = 7225, 85^1 = 85
|
||||
out = static_cast<int32_t>(c0 * 52200625u + c1 * 614125u + c2 * 7225u + c3 * 85u + c4);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Decode base85 string directly into vector (no intermediate buffer)
|
||||
bool base85_decode_int32_vector(const std::string &base85, std::vector<int32_t> &out) {
|
||||
size_t len = base85.size();
|
||||
if (len % 5 != 0)
|
||||
return false;
|
||||
/// Decode base64/base64url string directly into vector of little-endian int32 values
|
||||
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
|
||||
/// @param out Output vector (cleared and filled with decoded int32 values)
|
||||
/// @return true if successful, false if decode failed or invalid size
|
||||
bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t> &out) {
|
||||
// Decode in chunks to minimize stack usage
|
||||
constexpr size_t chunk_bytes = 48; // 12 int32 values
|
||||
constexpr size_t chunk_chars = 64; // 48 * 4/3 = 64 chars
|
||||
uint8_t chunk[chunk_bytes];
|
||||
|
||||
out.clear();
|
||||
const char *ptr = base85.data();
|
||||
const char *end = ptr + len;
|
||||
|
||||
while (ptr < end) {
|
||||
int32_t value;
|
||||
if (!base85_decode_int32(ptr, value))
|
||||
const uint8_t *input = reinterpret_cast<const uint8_t *>(base64.data());
|
||||
size_t remaining = base64.size();
|
||||
size_t pos = 0;
|
||||
|
||||
while (remaining > 0) {
|
||||
size_t chars_to_decode = std::min(remaining, chunk_chars);
|
||||
size_t decoded_len = base64_decode(input + pos, chars_to_decode, chunk, chunk_bytes);
|
||||
|
||||
if (decoded_len == 0)
|
||||
return false;
|
||||
out.push_back(value);
|
||||
ptr += 5;
|
||||
|
||||
// Parse little-endian int32 values
|
||||
for (size_t i = 0; i + 3 < decoded_len; i += 4) {
|
||||
int32_t timing = static_cast<int32_t>(encode_uint32(chunk[i + 3], chunk[i + 2], chunk[i + 1], chunk[i]));
|
||||
out.push_back(timing);
|
||||
}
|
||||
|
||||
// Check for incomplete int32 in last chunk
|
||||
if (remaining <= chunk_chars && (decoded_len % 4) != 0)
|
||||
return false;
|
||||
|
||||
pos += chars_to_decode;
|
||||
remaining -= chars_to_decode;
|
||||
}
|
||||
return true;
|
||||
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
// Colors
|
||||
|
||||
@@ -395,6 +395,28 @@ constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261UL;
|
||||
/// FNV-1 32-bit prime
|
||||
constexpr uint32_t FNV1_PRIME = 16777619UL;
|
||||
|
||||
/// Extend a FNV-1 hash with an integer (hashes each byte).
|
||||
template<std::integral T> constexpr uint32_t fnv1_hash_extend(uint32_t hash, T value) {
|
||||
using UnsignedT = std::make_unsigned_t<T>;
|
||||
UnsignedT uvalue = static_cast<UnsignedT>(value);
|
||||
for (size_t i = 0; i < sizeof(T); i++) {
|
||||
hash *= FNV1_PRIME;
|
||||
hash ^= (uvalue >> (i * 8)) & 0xFF;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
/// Extend a FNV-1 hash with additional string data.
|
||||
constexpr uint32_t fnv1_hash_extend(uint32_t hash, const char *str) {
|
||||
if (str) {
|
||||
while (*str) {
|
||||
hash *= FNV1_PRIME;
|
||||
hash ^= *str++;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
inline uint32_t fnv1_hash_extend(uint32_t hash, const std::string &str) { return fnv1_hash_extend(hash, str.c_str()); }
|
||||
|
||||
/// Extend a FNV-1a hash with additional string data.
|
||||
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str) {
|
||||
if (str) {
|
||||
@@ -1096,9 +1118,66 @@ std::string format_hex_pretty(T val, char separator = '.', bool show_length = tr
|
||||
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T), separator, show_length);
|
||||
}
|
||||
|
||||
/// Calculate buffer size needed for format_bin_to: "01234567...\0" = bytes * 8 + 1
|
||||
constexpr size_t format_bin_size(size_t byte_count) { return byte_count * 8 + 1; }
|
||||
|
||||
/** Format byte array as binary string to buffer.
|
||||
*
|
||||
* Each byte is formatted as 8 binary digits (MSB first).
|
||||
* Truncates output if data exceeds buffer capacity.
|
||||
*
|
||||
* @param buffer Output buffer to write to.
|
||||
* @param buffer_size Size of the output buffer.
|
||||
* @param data Pointer to the byte array to format.
|
||||
* @param length Number of bytes in the array.
|
||||
* @return Pointer to buffer.
|
||||
*
|
||||
* Buffer size needed: length * 8 + 1 (use format_bin_size()).
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* char buf[9]; // format_bin_size(1)
|
||||
* format_bin_to(buf, sizeof(buf), data, 1); // "10101011"
|
||||
* @endcode
|
||||
*/
|
||||
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length);
|
||||
|
||||
/// Format byte array as binary to buffer. Automatically deduces buffer size.
|
||||
template<size_t N> inline char *format_bin_to(char (&buffer)[N], const uint8_t *data, size_t length) {
|
||||
static_assert(N >= 9, "Buffer must hold at least one binary byte (9 chars)");
|
||||
return format_bin_to(buffer, N, data, length);
|
||||
}
|
||||
|
||||
/** Format an unsigned integer in binary to buffer, MSB first.
|
||||
*
|
||||
* @tparam N Buffer size (must be >= sizeof(T) * 8 + 1).
|
||||
* @tparam T Unsigned integer type.
|
||||
* @param buffer Output buffer to write to.
|
||||
* @param val The unsigned integer value to format.
|
||||
* @return Pointer to buffer.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* char buf[9]; // format_bin_size(sizeof(uint8_t))
|
||||
* format_bin_to(buf, uint8_t{0xAA}); // "10101010"
|
||||
* char buf16[17]; // format_bin_size(sizeof(uint16_t))
|
||||
* format_bin_to(buf16, uint16_t{0x1234}); // "0001001000110100"
|
||||
* @endcode
|
||||
*/
|
||||
template<size_t N, typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
|
||||
inline char *format_bin_to(char (&buffer)[N], T val) {
|
||||
static_assert(N >= sizeof(T) * 8 + 1, "Buffer too small for type");
|
||||
val = convert_big_endian(val);
|
||||
return format_bin_to(buffer, reinterpret_cast<const uint8_t *>(&val), sizeof(T));
|
||||
}
|
||||
|
||||
/// Format the byte array \p data of length \p len in binary.
|
||||
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
std::string format_bin(const uint8_t *data, size_t length);
|
||||
/// Format an unsigned integer in binary, starting with the most significant byte.
|
||||
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_bin(T val) {
|
||||
val = convert_big_endian(val);
|
||||
return format_bin(reinterpret_cast<uint8_t *>(&val), sizeof(T));
|
||||
@@ -1137,13 +1216,11 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string);
|
||||
size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len);
|
||||
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len);
|
||||
|
||||
/// Size of buffer needed for base85 encoded int32 (5 chars + null terminator)
|
||||
static constexpr size_t BASE85_INT32_ENCODED_SIZE = 6;
|
||||
|
||||
void base85_encode_int32(int32_t value, std::span<char, BASE85_INT32_ENCODED_SIZE> output);
|
||||
|
||||
bool base85_decode_int32(const char *input, int32_t &out);
|
||||
bool base85_decode_int32_vector(const std::string &base85, std::vector<int32_t> &out);
|
||||
/// Decode base64/base64url string directly into vector of little-endian int32 values
|
||||
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
|
||||
/// @param out Output vector (cleared and filled with decoded int32 values)
|
||||
/// @return true if successful, false if decode failed or invalid size
|
||||
bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t> &out);
|
||||
|
||||
///@}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#define ESPHOME_strncpy_P strncpy_P
|
||||
#define ESPHOME_strncat_P strncat_P
|
||||
#define ESPHOME_snprintf_P snprintf_P
|
||||
// Type for pointers to PROGMEM strings (for use with ESPHOME_F return values)
|
||||
using ProgmemStr = const __FlashStringHelper *;
|
||||
#else
|
||||
#define ESPHOME_F(string_literal) (string_literal)
|
||||
#define ESPHOME_PGM_P const char *
|
||||
@@ -19,4 +21,6 @@
|
||||
#define ESPHOME_strncpy_P strncpy
|
||||
#define ESPHOME_strncat_P strncat
|
||||
#define ESPHOME_snprintf_P snprintf
|
||||
// Type for pointers to strings (no PROGMEM on non-ESP8266 platforms)
|
||||
using ProgmemStr = const char *;
|
||||
#endif
|
||||
|
||||
@@ -28,8 +28,8 @@ dependencies:
|
||||
rules:
|
||||
- if: "target in [esp32s2, esp32s3, esp32p4]"
|
||||
esphome/esp-hub75:
|
||||
version: 0.2.2
|
||||
version: 0.3.0
|
||||
rules:
|
||||
- if: "target in [esp32, esp32s2, esp32s3, esp32p4]"
|
||||
- if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]"
|
||||
esp32async/asynctcp:
|
||||
version: 3.4.91
|
||||
|
||||
@@ -682,6 +682,7 @@ def lint_trailing_whitespace(fname, match):
|
||||
# Heap-allocating helpers that cause fragmentation on long-running embedded devices.
|
||||
# These return std::string and should be replaced with stack-based alternatives.
|
||||
HEAP_ALLOCATING_HELPERS = {
|
||||
"format_bin": "format_bin_to() with a stack buffer",
|
||||
"format_hex": "format_hex_to() with a stack buffer",
|
||||
"format_hex_pretty": "format_hex_pretty_to() with a stack buffer",
|
||||
"format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer",
|
||||
@@ -699,6 +700,7 @@ HEAP_ALLOCATING_HELPERS = {
|
||||
# get_mac_address(?!_) ensures we don't match get_mac_address_into_buffer, etc.
|
||||
# CPP_RE_EOL captures rest of line so NOLINT comments are detected
|
||||
r"[^\w]("
|
||||
r"format_bin(?!_)|"
|
||||
r"format_hex(?!_)|"
|
||||
r"format_hex_pretty(?!_)|"
|
||||
r"format_mac_address_pretty|"
|
||||
|
||||
@@ -9,6 +9,8 @@ alarm_control_panel:
|
||||
name: Alarm Panel
|
||||
codes:
|
||||
- "1234"
|
||||
- "5678"
|
||||
- "0000"
|
||||
requires_code_to_arm: true
|
||||
arming_home_time: 1s
|
||||
arming_night_time: 1s
|
||||
@@ -29,6 +31,7 @@ alarm_control_panel:
|
||||
name: Alarm Panel 2
|
||||
codes:
|
||||
- "1234"
|
||||
- "9999"
|
||||
requires_code_to_arm: true
|
||||
arming_home_time: 1s
|
||||
arming_night_time: 1s
|
||||
|
||||
@@ -5,7 +5,10 @@ wifi:
|
||||
udp:
|
||||
id: my_udp
|
||||
listen_address: 239.0.60.53
|
||||
addresses: ["239.0.60.53"]
|
||||
addresses:
|
||||
- "239.0.60.53"
|
||||
- "192.168.1.255"
|
||||
- "10.0.0.255"
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received %d bytes"
|
||||
|
||||
33
tests/integration/fixtures/udp_send_receive.yaml
Normal file
33
tests/integration/fixtures/udp_send_receive.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
esphome:
|
||||
name: udp-test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
services:
|
||||
- service: send_udp_message
|
||||
then:
|
||||
- udp.write:
|
||||
id: test_udp
|
||||
data: "HELLO_UDP_TEST"
|
||||
- service: send_udp_bytes
|
||||
then:
|
||||
- udp.write:
|
||||
id: test_udp
|
||||
data: [0x55, 0x44, 0x50, 0x5F, 0x42, 0x59, 0x54, 0x45, 0x53] # "UDP_BYTES"
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
udp:
|
||||
- id: test_udp
|
||||
addresses:
|
||||
- "127.0.0.1"
|
||||
- "127.0.0.2"
|
||||
port:
|
||||
listen_port: UDP_LISTEN_PORT_PLACEHOLDER
|
||||
broadcast_port: UDP_BROADCAST_PORT_PLACEHOLDER
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received UDP: %d bytes"
|
||||
args: [data.size()]
|
||||
171
tests/integration/test_udp.py
Normal file
171
tests/integration/test_udp.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Integration test for UDP component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
import contextlib
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@dataclass
|
||||
class UDPReceiver:
|
||||
"""Collects UDP messages received."""
|
||||
|
||||
messages: list[bytes] = field(default_factory=list)
|
||||
message_received: asyncio.Event = field(default_factory=asyncio.Event)
|
||||
|
||||
def on_message(self, data: bytes) -> None:
|
||||
"""Called when a message is received."""
|
||||
self.messages.append(data)
|
||||
self.message_received.set()
|
||||
|
||||
async def wait_for_message(self, timeout: float = 5.0) -> bytes:
|
||||
"""Wait for a message to be received."""
|
||||
await asyncio.wait_for(self.message_received.wait(), timeout=timeout)
|
||||
return self.messages[-1]
|
||||
|
||||
async def wait_for_content(self, content: bytes, timeout: float = 5.0) -> bytes:
|
||||
"""Wait for a specific message content."""
|
||||
deadline = asyncio.get_event_loop().time() + timeout
|
||||
while True:
|
||||
for msg in self.messages:
|
||||
if content in msg:
|
||||
return msg
|
||||
remaining = deadline - asyncio.get_event_loop().time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError(
|
||||
f"Content {content!r} not found in messages: {self.messages}"
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(self.message_received.wait(), timeout=remaining)
|
||||
self.message_received.clear()
|
||||
except TimeoutError:
|
||||
raise TimeoutError(
|
||||
f"Content {content!r} not found in messages: {self.messages}"
|
||||
) from None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]:
|
||||
"""Async context manager that listens for UDP messages.
|
||||
|
||||
Args:
|
||||
port: Port to listen on. 0 for auto-assign.
|
||||
|
||||
Yields:
|
||||
Tuple of (port, UDPReceiver) where port is the UDP port being listened on.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("127.0.0.1", port))
|
||||
sock.setblocking(False)
|
||||
actual_port = sock.getsockname()[1]
|
||||
|
||||
receiver = UDPReceiver()
|
||||
|
||||
async def receive_messages() -> None:
|
||||
"""Background task to receive UDP messages."""
|
||||
loop = asyncio.get_running_loop()
|
||||
while True:
|
||||
try:
|
||||
data = await loop.sock_recv(sock, 4096)
|
||||
if data:
|
||||
receiver.on_message(data)
|
||||
except BlockingIOError:
|
||||
await asyncio.sleep(0.01)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
task = asyncio.create_task(receive_messages())
|
||||
try:
|
||||
yield actual_port, receiver
|
||||
finally:
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
sock.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_udp_send_receive(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test UDP component can send messages with multiple addresses configured."""
|
||||
# Track log lines to verify dump_config output
|
||||
log_lines: list[str] = []
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
log_lines.append(line)
|
||||
|
||||
async with udp_listener() as (udp_port, receiver):
|
||||
# Replace placeholders in the config
|
||||
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
|
||||
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
|
||||
|
||||
async with (
|
||||
run_compiled(config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device is running
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "udp-test"
|
||||
|
||||
# Get services
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Test sending string message
|
||||
send_message_service = next(
|
||||
(s for s in services if s.name == "send_udp_message"), None
|
||||
)
|
||||
assert send_message_service is not None, (
|
||||
"send_udp_message service not found"
|
||||
)
|
||||
|
||||
await client.execute_service(send_message_service, {})
|
||||
|
||||
try:
|
||||
msg = await receiver.wait_for_content(b"HELLO_UDP_TEST", timeout=5.0)
|
||||
assert b"HELLO_UDP_TEST" in msg
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"UDP string message not received. Got: {receiver.messages}"
|
||||
)
|
||||
|
||||
# Test sending bytes
|
||||
send_bytes_service = next(
|
||||
(s for s in services if s.name == "send_udp_bytes"), None
|
||||
)
|
||||
assert send_bytes_service is not None, "send_udp_bytes service not found"
|
||||
|
||||
await client.execute_service(send_bytes_service, {})
|
||||
|
||||
try:
|
||||
msg = await receiver.wait_for_content(b"UDP_BYTES", timeout=5.0)
|
||||
assert b"UDP_BYTES" in msg
|
||||
except TimeoutError:
|
||||
pytest.fail(f"UDP bytes message not received. Got: {receiver.messages}")
|
||||
|
||||
# Verify we received at least 2 messages (string + bytes)
|
||||
assert len(receiver.messages) >= 2, (
|
||||
f"Expected at least 2 messages, got {len(receiver.messages)}"
|
||||
)
|
||||
|
||||
# Verify dump_config logged all configured addresses
|
||||
# This tests that FixedVector<const char*> stores addresses correctly
|
||||
log_text = "\n".join(log_lines)
|
||||
assert "Address: 127.0.0.1" in log_text, (
|
||||
f"Address 127.0.0.1 not found in dump_config. Log: {log_text[-2000:]}"
|
||||
)
|
||||
assert "Address: 127.0.0.2" in log_text, (
|
||||
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
|
||||
)
|
||||
Reference in New Issue
Block a user